#!/usr/bin/env python

# Copyright (c) 2007,2008,2009 Mike Taylor and Decklin Foster
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
twerp.py - standalone twitter post and friend list getter

That's all it does and it does that very simply :)

Usage:
    twerp [-U username] [-P password] [-c configfile] [-frpnm]
          [-t user] [-d user message] [message]

    twerp message           Post a status update to Twitter
    twerp -d foo bar        Post a direct message to foo with the text "bar"
    twerp -f                Pull recent entries in your friends timeline
    twerp -r                Pull your recent replies
    twerp -t user           Pull recent entires in a user's timeline
    twerp -p                Pull the recent public timeline

Requires:
    setuptools              http://cheeseshop.python.org/pypi/setuptools
    simplejson              http://cheeseshop.python.org/pypi/simplejson
"""

import os
import sys
import time
import ConfigParser
import optparse
import readline
import locale
import httplib
import urllib2
import urllib
import base64
import simplejson
from email.Utils import parsedate_tz, mktime_tz

from pyTwerp import __version__ as VERSION
from pyTwerp import __useragent__ as USERAGENT

class TwerpNoChanges(Exception): pass
class TwerpDataError(Exception): pass
class TwerpAccountError(Exception): pass
class TwerpHTTPError(Exception): pass

class TwerpErrorHandler(urllib2.HTTPDefaultErrorHandler):
    def http_error_304(self, req, fp, code, message, headers):
        raise TwerpNoChanges()
    def http_error_default(self, req, fp, code, msg, headers):
        raise TwerpHTTPError('server returned status %d' % code)

def fetch(url, auth=None, post=None, mod=None):
    if mod:
        since_param = '?since_id=%s' % mod
    else:
        since_param = ''

    request = urllib2.Request('%s%s' % (url, since_param))
    request.add_header('User-Agent', USERAGENT)

    if auth:
        request.add_header('Authorization', auth)
    if post:
        post.update({'source': 'pytwerp'})
        request.add_data(urllib.urlencode(post))

    try:
        opener = urllib2.build_opener(TwerpErrorHandler())
        return opener.open(request)
    except httplib.BadStatusLine, e:
        raise TwerpHTTPError('could not understand response "%s"' % e.line)
    except urllib2.URLError, e:
        raise TwerpHTTPError('could not open URL: %s' % e.reason)

def getpreferredencoding():
    if sys.platform == 'darwin':
        return 'utf-8'
    else:
        return locale.getpreferredencoding()

def unescape(s):
    s = s.replace("&lt;", "<")
    s = s.replace("&gt;", ">")
    s = s.replace("&apos;", "'")
    s = s.replace("&quot;", '"')
    s = s.replace("&amp;", "&")
    return s

def readMessage():
    if os.isatty(sys.stdin.fileno()):
        try:
            return raw_input('twerp> ')
        except EOFError:
            return ''
    else:
        return sys.stdin.read().strip()

def parseItem(json):
    result = {}

    for key in json:
        if key == 'user':
            for ukey in json['user']:
                result['user_%s' % ukey] = json['user'][ukey]
        elif key == 'text':
            result[key] = unescape(json[key])
        else:
            result[key] = json[key]
        if key == 'created_at':
            date = time.localtime(mktime_tz(parsedate_tz(json[key])))
            result['created_at_time'] = time.strftime('%H:%M', date)

    return result

def getPublic(url, since_id=None):
    url += '/statuses/public_timeline.json'
    return fetch(url, mod=since_id)

def getUser(auth, user, url, since_id=None):
    url += '/statuses/user_timeline/%s.json' % user
    return fetch(url, auth, mod=since_id)

def getFriends(auth, user, url, since_id=None):
    url += '/statuses/friends_timeline/%s.json' % user
    return fetch(url, auth, mod=since_id)

def getReplies(auth, url, since_id=None):
    url += '/statuses/replies.json'
    return fetch(url, auth, mod=since_id)

def postStatus(auth, msg, url):
    url += '/statuses/update.json'
    return fetch(url, auth, post={'status': msg})

def postDirect(auth, recip, msg, url):
    url += '/direct_messages/new.json'
    return fetch(url, auth, post={'user': recip, 'text': msg})

def showMessage(fp):
    try:
        data = simplejson.load(fp)
        if data.has_key('recipient'):
            print 'Message to %s posted as id %s' % (data['recipient'], data['id'])
        else:
            print 'Message posted as id %s' % data['id']
    except ValueError, e:
        raise TwerpDataError(e.args[0])

def showTimeline(fp, template, sanitize=False):
    result = None
    try:
        data = simplejson.load(fp)
        data.reverse()
        for item in data:
            entry = parseItem(item)
            text  = template % entry
            if sanitize:
                text = text.replace('\n', ' ')

            print text.encode(getpreferredencoding(), 'replace')

            result = entry['id']
    except (AttributeError, ValueError), e:
        raise TwerpDataError(e.args[0])

    return result

def checkOptions():
    config = os.path.expanduser(os.path.join('~', '.twerprc'))
    op     = optparse.OptionParser(version='twerp %s' % VERSION)

    op.add_option('-c', '--config',   dest='confpath', default=config)
    op.add_option('-v', '--verbose',  dest='verbose',  default=False, action='store_true')
    op.add_option('-n', '--no-save-conf', dest='save_conf', default=True, action='store_false')
    op.add_option('-m', '--no-sanitize',  dest='sanitize',  default=True, action='store_false')
    op.add_option('-U', '--username', dest='username', default=None)
    op.add_option('-P', '--password', dest='password', default=None)
    op.add_option('-T', '--template', dest='template', default=None)
    op.add_option('-s', '--since',    dest='since_id', default=None)
    op.add_option('-d', '--direct',   dest='direct',   default=None)
    op.add_option('-t', '--timeline', dest='timeline', default=None)
    op.add_option('-u', '--url',      dest='url',      default=None)
    op.add_option('-f', '--friends',  dest='friends',  default=False, action='store_true')
    op.add_option('-r', '--replies',  dest='replies',  default=False, action='store_true')
    op.add_option('-p', '--public',   dest='public',   default=False, action='store_true')

    options, args = op.parse_args()
    options.msg = ' '.join(args)

    cp = ConfigParser.RawConfigParser()
    cp.add_section('account')
    cp.add_section('display')
    cp.add_section('checked') # deprecated since Twitter decided to screw folks who know how to make HTTP calls
    cp.add_section('last_id')
    cp.read([options.confpath])

    try:
        if options.password is None:
            options.password = cp.get('account', 'password')
        if options.username is None:
            options.username = cp.get('account', 'username')
    except ConfigParser.NoOptionError:
        pass
    if options.username is None or options.password is None:
        raise TwerpAccountError()

    try:
        if options.verbose is False:
            options.verbose = cp.getboolean('display', 'verbose')
    except ConfigParser.NoOptionError:
        pass

    try:
        if options.url is None:
            options.url = cp.get('account', 'url')
    except ConfigParser.NoOptionError:
        options.url = 'http://twitter.com'

    options.url = options.url.rstrip('/')

    try:
        if options.template is None:
            options.template = cp.get('display', 'template')
    except ConfigParser.NoOptionError:
        options.template = '%(user_screen_name)s: %(text)s'

    return options, cp

def saveConfig(options, cp, last_id):
    if not cp.has_option('account', 'username'):
        cp.set('account', 'username', options.username)
    if not cp.has_option('account', 'password'):
        cp.set('account', 'password', options.password)
    if not cp.has_option('display', 'verbose'):
        cp.set('display', 'verbose', options.verbose)
    if not cp.has_option('display', 'template'):
        cp.set('display', 'template', options.template)
    for k, v in last_id.items():
        cp.set('last_id', k, v)

    # always remember last used api url
    cp.set('account', 'url', options.url)

    originalMask = os.umask(0077)
    try:
        cp.write(file(options.confpath, 'w'))
    finally:
        os.umask(originalMask)

def main():
    try:
        options, cp = checkOptions()
    except TwerpAccountError:
        print >>sys.stderr, 'No account specified. Please run twerp once ' + \
                            'with --username and --password\n' + \
                            'to create your ~/.twerprc.'
        sys.exit(2)

    digest  = base64.encodestring('%s:%s' % (options.username, options.password))
    auth    = 'Basic %s' % digest[:-1]
    last_id = dict(cp.items('last_id'))

    def doTimeline(t, func, **kwargs):
        since_id   = options.since_id or last_id.get(t)
        response   = func(since_id=since_id, **kwargs)
        last_id[t] = showTimeline(response, options.template, options.sanitize)

    def doMessage(m, func, **kwargs):
        if not m:
            m = readMessage()
        if len(m) == 0:
            return
        if len(m) > 140:
            print >>sys.stderr, 'Message %d chars too long.' % (len(m) - 140)
            sys.exit(2)
        response = func(msg=m, **kwargs)
        if options.verbose: showMessage(response)

    try:
        if options.public:
            doTimeline('_public', getPublic, url=options.url)
        elif options.replies:
            doTimeline('_replies', getReplies, auth=auth,
                                               url=options.url)
        elif options.friends:
            doTimeline('_friends', getFriends, auth=auth,
                                               user=options.username,
                                               url=options.url)
        elif options.timeline:
            doTimeline(options.timeline, getUser, auth=auth,
                                                  user=options.timeline,
                                                  url=options.url)
        elif options.direct:
            doMessage(options.msg, postDirect, auth=auth,
                                               recip=options.direct,
                                               url=options.url)
        else:
            doMessage(options.msg, postStatus, auth=auth,
                                               url=options.url)

        if options.save_conf:
            saveConfig(options, cp, last_id)
    except KeyboardInterrupt:
        sys.exit(130)
    except TwerpNoChanges:
        if options.verbose: print 'No changes since last run.'
        saveConfig(options, cp, last_id)
        sys.exit(1)
    except TwerpDataError, e:
        print >>sys.stderr, 'Problem reading data: %s' % e.args[0]
        sys.exit(3)
    except TwerpHTTPError, e:
        print >>sys.stderr, 'HTTP error: %s.' % e.args[0]
        sys.exit(3)

if __name__ == "__main__":
    main()

